/**
 * This file is part of the eConference project and it is distributed under the 
 * terms of the MIT Open Source license.
 * 
 * The MIT License
 * Copyright (c) 2005 Collaborative Development Group - Dipartimento di Informatica, 
 *                    University of Bari, http://cdg.di.uniba.it
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this 
 * software and associated documentation files (the "Software"), to deal in the Software 
 * without restriction, including without limitation the rights to use, copy, modify, 
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to the following 
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies 
 * or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 
 * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package it.uniba.di.cdg.jabber;

import it.uniba.di.cdg.jabber.internal.BuddyRoster;
import it.uniba.di.cdg.jabber.internal.CustomInvitationEvent;
import it.uniba.di.cdg.jabber.internal.JabberChatService;
import it.uniba.di.cdg.xcore.chat.ChatEvent;
import it.uniba.di.cdg.xcore.chat.ChatMessage;
import it.uniba.di.cdg.xcore.chat.IChatMessage;
import it.uniba.di.cdg.xcore.chat.IChatService;
import it.uniba.di.cdg.xcore.chat.IChatService.ChatContext;
import it.uniba.di.cdg.xcore.network.BackendException;
import it.uniba.di.cdg.xcore.network.IBackend;
import it.uniba.di.cdg.xcore.network.INetworkBackendHelper;
import it.uniba.di.cdg.xcore.network.ServerContext;
import it.uniba.di.cdg.xcore.network.UserContext;
import it.uniba.di.cdg.xcore.network.events.BackendStatusChangeEvent;
import it.uniba.di.cdg.xcore.network.events.IBackendEvent;
import it.uniba.di.cdg.xcore.network.messages.IMessage;
import it.uniba.di.cdg.xcore.network.messages.SystemMessage;
import it.uniba.di.cdg.xcore.network.model.IBuddyRoster;
import it.uniba.di.cdg.xcore.network.services.Capabilities;
import it.uniba.di.cdg.xcore.network.services.ICapabilities;
import it.uniba.di.cdg.xcore.network.services.ICapability;
import it.uniba.di.cdg.xcore.network.services.INetworkService;
import it.uniba.di.cdg.xcore.network.services.INetworkServiceContext;

import java.util.Map;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.ConfigurationScope;
import org.jivesoftware.smack.AccountManager;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smackx.muc.InvitationListener;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.osgi.service.prefs.Preferences;

/**
 * Jabber/XMPP backend implementation. A backend may be extended by providing additional services through 
 * the use of the <code>it.uniba.di.cdg.jabber.services</code> extension point.
 * 
 * FIXME ConnectionEstablishedListener doesn't work. Check this again with SMACK 2.1.0.
 * FIXED: ConnectionEstablishedListener class is not present in the 3.1.0 version of Smack, so it has removed
 */
public class JabberBackend implements IBackend, PacketListener,
        ConnectionListener {
    /**
     * This backend's unique id.
     */
    public static final String ID = JabberPlugin.ID + ".jabberBackend";

    /**
     * The capabilities supported by this backend (these depend on the implemented feature set). 
     */
    private final ICapabilities capabilities;

    /**
     * Connection to the XMPP server: it can be used for creating new chats, adding filters, callbacks, ...
     */
    private XMPPConnection connection;

    /**
     * The roster is the core 
     */
    private BuddyRoster buddies;

    /**
     * Provides useful methods.
     */
    private INetworkBackendHelper helper;

    // Multi-user chat setup
    private InvitationListener invitationListener = new InvitationListener() {
        public void invitationReceived( XMPPConnection connection, String room, String inviter,
                String reason, String password, Message message ) {
            if (JabberBackend.this.connection == connection) {
                final IMessage m = convertFromSmack( message );
                notifyEventListeners( new CustomInvitationEvent( JabberBackend.this, ID, room,
                        inviter, reason, password, m ) );
            }
        }
    };

    /**
     * The context information related to the server we are currently connected. It is initialized by
     * {@see #connect(ServerContext, UserAccount)}. When disconneted it should be <code>null</code>. 
     */
    private ServerContext serverContext;

    /**   
     * The user account we are currently authenticated with. It is initialized by
     * {@see #connect(ServerContext, UserAccount)}. When disconneted it should be <code>null</code>.
     */
    private UserContext userAccount;

    /**
     * Create a new jabber backend.
     */
    public JabberBackend() {
        super();
        this.capabilities = new Capabilities();
        this.buddies = new BuddyRoster( this );

        // Chat, multi-chat and e-conference support are built-in.
//        capabilities.add( IChatService.CHAT_SERVICE );
//        capabilities.add( IMultiChatService.MULTI_CHAT_SERVICE );
//        capabilities.add( IEConferenceService.ECONFERENCE_SERVICE );        
    }

    /**
     * Create a normal XMPP connection. Clients are not expected to re-implement this method:
     * it is used as an hook for testing.
     * 
     * @param host
     * @param port
     * @param serviceName this is not required, you can use the empty string
     * @return a new XMPP connection
     * @throws XMPPException
     */
    protected XMPPConnection createConnection( String host, int port, String serviceName ) throws XMPPException {
    	ConnectionConfiguration config;
    	if (serviceName=="" || serviceName == null) 
    		config = new ConnectionConfiguration(host, port);
    	else
    		config = new ConnectionConfiguration(host, port, serviceName);
	//TODO: verify how the following statement works
	config.setReconnectionAllowed(true);

        return new XMPPConnection( config );
    }

    /**
     * Create an SSL XMPP connection. Clients are not expected to re-implement this method:
     * it is used as an hook for testing.
     * 
     * @param host
     * @param port
     * @param serviceName this is not required, you can use the empty string
     * @return a new XMPP connection
     * @throws XMPPException
     */
    protected XMPPConnection createSecureConnection( String host, int port, String serviceName ) throws XMPPException {
    	ConnectionConfiguration config;
    	if (serviceName=="" || serviceName == null) 
    		config = new ConnectionConfiguration(host, port);
    	else
    		config = new ConnectionConfiguration(host, port, serviceName);
    	config.setCompressionEnabled(true);
    	config.setSASLAuthenticationEnabled(true);    	
    	//TODO: verify how the following statement works
    	config.setReconnectionAllowed(true);    	
    	return new XMPPConnection(config);
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#connect(it.uniba.di.cdg.xcore.network.ServerContext, it.uniba.di.cdg.xcore.network.UserAccount)
     */
    public void connect( final ServerContext ctx, final UserContext userAccount )
            throws BackendException {
        disconnect();
        this.serverContext = ctx;
        this.userAccount = userAccount;

        // TODO make the strings constants 
        Preferences preferences = new ConfigurationScope().getNode( "it.uniba.di.cdg.xcore.ui" );
        XMPPConnection.DEBUG_ENABLED = preferences.getBoolean(
                "it.uniba.di.cdg.xcore.ui.preferences_xmppbackend_showdebugger", false );

        try {        	
         	if(userAccount.isGmail()){
         		serverContext = ServerContext.GOOGLE_TALK;
         	}         	
            if (ctx.isSecure())              	
                connection = createSecureConnection( serverContext.getServerHost(), ctx.getPort(), serverContext.getServiceName() );            	
            else
                connection = createConnection( serverContext.getServerHost(), ctx.getPort(), serverContext.getServiceName());//serverContext.getServiceName() );
         	                     
            // This doesn't work ...
            //XMPPConnection.addConnectionListener( (ConnectionEstablishedListener) this );

            connection.connect();  
           
            connection.addConnectionListener(this);            
            connection.login( userAccount.getId(), userAccount.getPassword() );
            buddies.setJabberRoster( connection.getRoster() );
        } catch (XMPPException e) {        	
        	//TODO: inserire il metodo per avviare la finestra di riconnessione automatica
            throw new BackendException( e );
        }
        // [CHAT] We need a way to monitor incoming traffic for generating "Wanna chat" events.
        PacketFilter filter = new PacketTypeFilter( Message.class );
        connection.addPacketListener( this, filter );

        MultiUserChat.addInvitationListener( connection, invitationListener );

       
        // FIXME XMPPConnection.addConnectionListener( (ConnectionEstablishedListener) this ) does nothing :S
        connectionEstablished( connection );
    }

    /* (non-Javadoc)
     * @see org.jivesoftware.smack.PacketListener#processPacket(org.jivesoftware.smack.packet.Packet)
     */
    public void processPacket( Packet packet ) {
        final Message smackMessage = (Message) packet;

        IMessage message = convertFromSmack( smackMessage );
        if (message instanceof IChatMessage) {
            notifyEventListeners( new ChatEvent( ID, (IChatMessage) message ) );
        }
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#disconnect()
     */
    public void disconnect() {
        if (connection == null)
            return;
        connection.disconnect();
        connection = null;

        connectionClosed();
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#isConnected()
     */
    public boolean isConnected() {
        return connection != null && connection.isConnected();
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#getRoster()
     */
    public IBuddyRoster getRoster() {
        return buddies;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#getUserAccount()
     */
    public UserContext getUserAccount() {
        return userAccount;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#getServerContext()
     */
    public ServerContext getServerContext() {
        return serverContext;
    }

    /* (non-Javadoc)
     * @see org.jivesoftware.smack.ConnectionEstablishedListener#connectionEstablished(org.jivesoftware.smack.XMPPConnection)
     */
    public void connectionEstablished( XMPPConnection connection ) {
        notifyOnline();
    }

    /* (non-Javadoc)
     * @see org.jivesoftware.smack.ConnectionListener#connectionClosed()
     */
    public void connectionClosed() {
        notifyOffline();

        getRoster().clear();
        this.serverContext = null;
        this.userAccount = null;
    }

    /* (non-Javadoc)
     * @see org.jivesoftware.smack.ConnectionListener#connectionClosedOnError(java.lang.Exception)
     */
    public void connectionClosedOnError( Exception ex ) {
        System.err.println( "Connection closed because of an error: " + ex.getMessage() );
        // TODO Should diplay and error dialog and clear state.
        notifyOffline();
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#getConnectJob(it.uniba.di.cdg.xcore.network.ServerContext, it.uniba.di.cdg.xcore.network.UserAccount)
     */
    public Job getConnectJob( final ServerContext serverContext, final UserContext account ) {
        final Job connectJob = new Job( "Connecting ..." ) {
            @Override
            protected IStatus run( IProgressMonitor monitor ) {
                try {
                    connect( serverContext, account );
                } catch (BackendException e) {
                    e.printStackTrace();
                    return Status.CANCEL_STATUS;
                }
                return Status.OK_STATUS;
            }
        };
        return connectJob;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#getCapabilities()
     */
    public ICapabilities getCapabilities() {
        return capabilities;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#createService(it.uniba.di.cdg.xcore.network.services.ICapability, it.uniba.di.cdg.xcore.network.services.INetworkServiceContext)
     */
    public INetworkService createService( ICapability service, INetworkServiceContext context )
            throws BackendException {
        if (IChatService.CHAT_SERVICE.equals( service ))
            return new JabberChatService( (ChatContext) context, this );
//        else if (IMultiChatService.MULTI_CHAT_SERVICE.equals( service ))
//            return new JabberMultiChatService( (MultiChatContext) context, this );
//        else if (IEConferenceService.ECONFERENCE_SERVICE.equals( service ))
//            return new JabberEConferenceService( (EConferenceContext) context, this );
//        else if (IPlanningPokerService.PLANNINGPOKER_SERVICE.equals( service ))
//            return new JabberPlanningPokerService( (PlanningPokerContext) context, this );
        INetworkService networkService = findInServiceExtensionPoint(service, context);
        if (networkService!=null)
        	return networkService;
        throw new BackendException( String.format( "Unknown service (%s) requested", service ) );
    }

    private INetworkService findInServiceExtensionPoint(ICapability service,INetworkServiceContext context ) {
    	try {
    		ServiceRegistry registry = JabberPlugin.getDefault().getRegistry();
    		INetworkService networkService = registry.getService(service.getName());
    		networkService.setBackend(this);
    		networkService.setContext(context);
    		return networkService;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}

    	
		
	}

	/* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#setHelper(it.uniba.di.cdg.xcore.network.INetworkBackendHelper)
     */
    public void setHelper( INetworkBackendHelper helper ) {
        this.helper = helper;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.IBackend#getHelper()
     */
    public INetworkBackendHelper getHelper() {
        return helper;
    }

    /**
     * Returns the XMPP connection. This method is meant to be used by extensions.
     * 
     * @return the XMPP connection
     */
    public XMPPConnection getConnection() {
        return connection;
    }

    /**
     * Returna the user jid. If the backend has no connection then it returns <code>null</code>.
     * 
     * @return a string like <code>harry@ugres.di.uniba.it/Smack</code>
     */
    public String getUserJid() {
        if (connection != null)
            return connection.getUser();
        return null;
    }

    /**
     * Convert from a SMACK message to a normal <code>IMessage</code>.
     * TODO Add more SMACK message types and checks here
     * 
     * @param smackMessage
     * @return the converted message or <code>null</code> if it doesn't know how to convert it
     */
    public IMessage convertFromSmack( Message smackMessage ) {
        IMessage message = null;

        // Cripple messages with empty body: they are often used for "secret functions"
        if (smackMessage.getBody() == null || smackMessage.getBody().length() == 0)
            return null;

        if (Message.Type.chat.equals( smackMessage.getType() )
                || Message.Type.normal.equals( smackMessage.getType() )) {
            message = new ChatMessage( smackMessage.getThread(), smackMessage.getFrom(),
                    smackMessage.getBody(), smackMessage.getSubject() );
        } else if (Message.Type.error.equals( smackMessage.getType() )) {
            message = new SystemMessage( smackMessage.getBody() );
        }
        return message;
    }

    /**
     * Notify listeners that this backend has gone online.
     */
    private void notifyOnline() {
        notifyEventListeners( new BackendStatusChangeEvent( ID, true ) );
    }

    /**
     * Notify listeners that this backend has gone offline.
     */
    private void notifyOffline() {
        notifyEventListeners( new BackendStatusChangeEvent( ID, false ) );
    }

    /**
     * Notify all event listeners about an event generated by this backend.
     * 
     * @param event
     */
    private void notifyEventListeners( IBackendEvent event ) {
        helper.notifyBackendEvent( event );
    }
    
    @Override
	public void changePassword(String newpasswd) throws Exception {
		AccountManager man = new AccountManager(this.connection);
		man.changePassword(newpasswd);
		
	}

	@Override
	public void reconnectingIn(int arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void reconnectionFailed(Exception arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void reconnectionSuccessful() {
		notifyOnline();
	}

	@Override
	public void registerNewAccount(String userId, String password, ServerContext server, Map<String,String> info) throws Exception{
		XMPPConnection conn = new XMPPConnection(server.getServiceName());
		conn.connect();
		conn.getAccountManager().createAccount(userId, password, info);		
		conn.disconnect();
		
	}
}
